package breadbox.client.files;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

/**
 * Divides an input stream into chunks
 * @author Forrest
 *
 */
public class Chunker {
	// constants
	private static final int DEFAULT_CHUNKSIZE = 16384;		// 16 KB
	private static final int MIN_CHUNKSIZE = 4096;			// 4 KB
	private static final int MAX_CHUNKSIZE = 1048576;		// 1 MB
	private static final int CHUNK_OVERHEAD = 256;			// metadata size in chunk
	
	// local variables
	private int chunk_size;				// current chunk size	
	private FileInput fs;				// filestream for current set of chunks
	private ArrayList<Chunk> chunks;	// chunks for current file
	private int current;				// index of current chunk
	@SuppressWarnings("unused")
	private boolean encryption, hashcheck, publicfile;
	
	/**
	 * Constructs a new Chunker
	 * @param fs FileStream object with byte stream for file
	 * @param chunksize requested chunksize in KB
	 * @param encryption indicates if encryption is enabled
	 * @param hashcheck indicates if hash checking is enabled
	 */
	public Chunker(FileInput fs, int chunksize, 
			boolean encryption, boolean hashcheck, boolean publicfile) {
		this.fs = fs;
		chunk_size = this.getChunkSize(chunksize * 1024);		
		
		this.encryption = encryption;
		this.hashcheck = hashcheck;
		try {
			this.chunks = this.chunkify();
		} catch (IOException e) {
			System.err.println("Error chunking file");
		}
		this.current = 0;
	}
	
	/**
	 * Get the chunk size for the current chunker
	 * @param c
	 * @return int chunksize, as multiple of MIN_CHUNKSIZE
	 */
	private int getChunkSize(int c) {
		// if not specified, set as default
		if (c < 1) {
			c = DEFAULT_CHUNKSIZE;
		}
		
		// enforce min/max chunksize on c
		c = Math.max(c, MIN_CHUNKSIZE);
		c = Math.min(c, MAX_CHUNKSIZE);
		
		// enforce as multiple of 4K
		int div = c / MIN_CHUNKSIZE;
		c = MIN_CHUNKSIZE * div;
		
		// subtract overhead
		c -= CHUNK_OVERHEAD;
		return c;
	}
	
	/**
	 * Returns the FileStream associated with this Chunker
	 * @return FileStream fs
	 */
	public FileInput getFileStream() {
		return fs;
	}
	
	/**
	 * Return all the chunks for the current file
	 * @return ArrayList<Chunk> chunks
	 */
	public ArrayList<Chunk> getChunks() {
		return chunks;
	}
	
	/**
	 * Returns the next Chunk in the list
	 * @return Chunk c
	 */
	public Chunk getNextChunk() {
		Chunk c = chunks.get(current);
		current++;
		return c;
	}
	
	/**
	 * True if another chunk exists, else False
	 * @return Boolean success
	 */
	public Boolean hasNext() {
		return current < chunks.size();
	}
	
	/**
	 * Divide the current stream into chunks
	 * @return ArrayList<Chunk> list of Chunks created
	 * @throws IOException 
	 */
	private ArrayList<Chunk> chunkify() throws IOException {
		// set up variables
		ArrayList<Chunk> chunks = new ArrayList<Chunk>();
		FileIDGenerator idGen = new FileIDGenerator();
		InputStream data_in = this.fs.getData();
		int current_read = 0, index = 0;
		
		// loop while input exists
		while (true) {
			int bytes_read = 0;
			byte[] b = new byte[chunk_size];
			// read up to chunk_size bytes
			while (bytes_read < chunk_size) {
				current_read = data_in.read(b, bytes_read,
						chunk_size - bytes_read);
				if (current_read == -1) {
					break;
				} else {
					bytes_read += current_read;
				}
			}
			// write to file if bytes were read
			if (bytes_read > 0) {
				FileChunk fc = null;
				// trim extra bytes from buffer at end of file
				if (current_read == -1) {
					byte[] data = new byte[bytes_read];
					for (int i = 0; i < bytes_read; i++) {
						data[i] = b[i];
					}
					fc = new FileChunk(data, index, idGen.generateId(),
							fs.getFilename(), encryption, publicfile);
				} else {
					fc = new FileChunk(b, index, idGen.generateId(),
							fs.getFilename(), encryption, publicfile);
				}
				// add the chunk to the list of chunks
				chunks.add(fc);
				//System.out.println("\tChunk id: " + fc.getID());
				index++;
			}
			// stop looping at EOF
			if (current_read == -1) {
				break;
			}
		}
		
		int total = chunks.size();
		
		// handle empty files
		if (total == 0) {
			return null;
		}
		
		// set the total field on each chunk
		for (Chunk c : chunks) {
			c.setTotal(total);
		}
		return chunks;
	}
}
